package com.jxpanda.commons.base;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.EncryptUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.jxpanda.commons.constant.StringConstant;
import com.jxpanda.commons.toolkit.DateTimeKit;
import com.jxpanda.commons.toolkit.EnumKit;
import com.jxpanda.commons.toolkit.ObjectKit;
import com.jxpanda.commons.toolkit.StringKit;
import lombok.*;

import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("unchecked")
public class Seeker<T extends Entity> {

    /**
     * 筛选条件列表（一群探机）
     */
    private List<Probe> probes = new ArrayList<>();

    /**
     * 排序条件列表（一群分拣机）
     */
    private List<Sorter> sorters = new ArrayList<>();

    /**
     * 分页对象
     */
    private Pagination<T> pagination = new Pagination<>();

    public String hash() {
        StringBuilder keyBuilder = new StringBuilder();
        getProbes().forEach(probe -> {
            keyBuilder.append(probe.getField());
            keyBuilder.append(probe.getRule());
            keyBuilder.append(probe.getValue());
            keyBuilder.append(probe.getSynapse());
        });
        getSorters().forEach(sorter -> {
            keyBuilder.append(sorter.getField());
            keyBuilder.append(sorter.getSorting());
        });
        keyBuilder.append(getPagination().getCurrent());
        keyBuilder.append(getPagination().getSize());
        return EncryptUtils.md5Base64(keyBuilder.toString());
    }

    /**
     * 用字段名来获取探机对象，方便外部修改
     * 如果没有，则创建一个，但是不添加到探机列表里
     */
    public Probe getProbe(@NonNull String field) {
        return getProbes().stream()
                .filter(probe -> field.equals(probe.getField()))
                .findFirst()
                .orElse(new Probe().withField(field));
    }

    /**
     * 用字段名来删除探机对象
     */
    public void removeProbe(@NonNull String field) {
        this.probes.removeIf(probe -> probe.getField().equals(field));
    }

    /**
     * 增加EQ条件的探机
     * 这个用的比较多，暂时先只写这个
     */
    public Seeker<T> eq(String field, Object value) {
        return addProbe(Rule.EQ, field, value);
    }

    /**
     * 增加GT条件的探机
     * 这个用的比较多，暂时先只写这个
     */
    public Seeker<T> gt(String field, Object value) {
        return addProbe(Rule.GT, field, value);
    }

    public Seeker<T> addProbe(Probe probe) {
        getProbes().add(probe);
        return this;
    }

    public Seeker<T> addProbe(Rule rule, String field, Object value) {
        getProbes().add(Probe.builder().field(field).value(value).rule(rule).build());
        return this;
    }

    public Seeker<T> addProbe(Rule rule, String field, Object value, Extend extend) {
        getProbes().add(Probe.builder().field(field).value(value).rule(rule).extend(extend).build());
        return this;
    }

    public List<Sorter> getSorters() {
        // null和empty的判断要分开，不然empty的时候也会重复创建一个空对象
        if (sorters == null) {
            sorters = new ArrayList<>();
        }
        return sorters;
    }

    /**
     * 设置排序
     * 如果已经有字段了，则修改排序规则
     * 否则，添加到排序列表里
     */
    public Seeker<T> setSorter(String field, Sorting sorting) {
        AtomicBoolean matches = new AtomicBoolean(false);
        getSorters().forEach(sorter -> {
            if (sorter.getField().equals(field)) {
                matches.set(true);
                sorter.setSorting(sorting);
            }
        });
        if (!matches.get()) {
            addSorter(field, sorting);
        }
        return this;
    }

    /**
     * 删除一个排序规则
     */
    public Seeker<T> removeSorter(String field) {
        getSorters().removeIf(sorter -> sorter.getField().equals(field));
        return this;
    }

    /**
     * 添加一个排序规则
     */
    @SuppressWarnings("UnusedReturnValue")
    public Seeker<T> addSorter(String field, Sorting sorting) {
        getSorters().add(new Sorter(field, sorting));
        return this;
    }

    /**
     * 构建成mybatis-plus的QueryWrapper对象
     * 就可以利用mybatis-plus做查询了
     * 整个Seeker对象的核心函数就是这个了
     * 用下来基本够用，暂时没必要优化
     */
    public QueryWrapper<T> buildWrapper(Class<T> clazz) {
        var queryWrapper = new QueryWrapper<T>();
        this.getProbes().stream()
                // SKIP掉或者字段名为空的不处理
                .filter(probe -> !Extend.SKIP.equals(probe.getExtend()) || probe.getField().isBlank())
                .forEach(probe -> {
                    // 先做探机的预处理
                    probe = probe.getExtend().handle(probe, clazz);
                    // 如果有OR条件拼接，执行OR条件（复杂的AND和OR条件拼接暂时不实现）
                    if (Synapse.OR.equals(probe.getSynapse())) {
                        probe.getSynapse().execute(queryWrapper);
                    }
                    // 执行rule上面的逻辑
                    probe.getRule().execute(queryWrapper, probe.getField(), probe.getJsonKey(), probe.getValue());
                });
        // 构建排序
        // 如果外部没有传递排序，默认创建一个基于创建时间倒序的排序
        if (ObjectKit.isEmpty(this.getSorters())) {
            this.addSorter(Entity.CREATED_DATE, Sorting.DESC);
        }
        this.getSorters().forEach(sorter -> {
            if (!sorter.getField().isBlank()) {
                sorter.getSorting().execute(queryWrapper, sorter.getField());
            }
        });
        return queryWrapper;
    }

    @Data
    @With
    @Builder
    @NoArgsConstructor
    public static class Probe {
        /**
         * 字段名
         */
        @Builder.Default
        private String field = StringConstant.BLANK;
        /**
         * json类型字段的key（仅json类型需要使用）
         */
        @Builder.Default
        private String jsonKey = StringConstant.BLANK;
        /**
         * 只有值是允许为空的
         */
        private Object value;
        /**
         * 查询规则，默认是EQ(=)
         */
        @Builder.Default
        private Rule rule = Rule.EQ;
        /**
         * 扩展字段
         * 这个字段比较灵活
         * 可以用来处理一切预处理逻辑
         * 也可以预留以后需要扩展逻辑的时候使用
         */
        @Builder.Default
        private Extend extend = Extend.NONE;
        /**
         * 突触，探机与探机之间的链接逻辑
         * AND和OR两个取值
         * 默认是AND（与）逻辑
         */
        @Builder.Default
        private Synapse synapse = Synapse.AND;

        /**
         * 是否自动把字段转为snakeCase
         * 默认是true，自动转换把filed进行一次snakeCase的转换
         */
        @Builder.Default
        private boolean snakeCase = true;

        /**
         * 手动写一下全参数构造器
         * 对所有字段做空安全处理
         * 反编译了lombok的代码后发现
         * 如果要严格的管理空安全的问题
         * 提供字段的默认值还有一个原因，lombok的无参构造器会自动把默认值设置好
         * 需要提供值的默认值（对于builder是必须的）
         * 然后就是提供全参数构造器，在全参数构造器中处理默认值
         * 这样不论是builder模式还是with函数，对象的值属性都能保证是空安全的
         * 因为build()函数和with函数都最终调用了全参数构造器
         */
        public Probe(String field, String jsonKey, Object value, Rule rule, Extend extend, Synapse synapse, boolean snakeCase) {
            this.field = field == null ? StringConstant.BLANK : field;
            this.jsonKey = jsonKey == null ? StringConstant.BLANK : jsonKey;
            // 只有value允许为空
            this.value = value;
            this.rule = rule == null ? Rule.EQ : rule;
            this.extend = extend == null ? Extend.NONE : extend;
            this.synapse = synapse == null ? Synapse.AND : synapse;
            this.snakeCase = snakeCase;
        }

        public String getField() {
            return snakeCase ? StringKit.snakeCase(this.field) : this.field;
        }

    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Sorter {
        /**
         * 排序字段，用哪个字段排序
         */
        @NonNull
        private String field;
        /**
         * 排序，两个取值，ASC（正序）、DESC（倒序）
         * 默认正序排序
         */
        @NonNull
        private Sorting sorting;


        static Sorter build(Object field, Object sorting) {
            return new Sorter((String) field, (Sorting) sorting);
        }

    }


    @Getter
    @AllArgsConstructor
    public enum Rule implements MethodEnum {
        /**
         * 等于
         */
        EQ(takeMethod("eq", Object.class, Object.class)),
        /**
         * 不等于
         */
        NE(takeMethod("ne", Object.class, Object.class)),
        GT(takeMethod("gt", Object.class, Object.class)),
        GE(takeMethod("ge", Object.class, Object.class)),
        LT(takeMethod("lt", Object.class, Object.class)),
        LE(takeMethod("le", Object.class, Object.class)),
        IN(takeMethod("in", Object.class, Collection.class)),
        NOT_IN(takeMethod("notIn", Object.class, Collection.class)),
        LIKE(takeMethod("like", Object.class, Object.class)),
        BETWEEN(takeMethod("between", Object.class, Object.class, Object.class)) {
            /**
             * Between函数的执行代理需要重写
             * 因为传入的参数是两个
             * 约定：入参传入一个数组(["begin","end"])，第一个值是初值，第二个值是终值
             * */
            @Override
            @SneakyThrows
            public <T> void execute(QueryWrapper<T> queryWrapper, String field, Object param) {
                this.execute(queryWrapper, field, StringConstant.BLANK, param);
            }

            @Override
            @SneakyThrows
            public <T> void execute(QueryWrapper<T> queryWrapper, String field, String jsonKey, Object param) {
                List<Object> params = (List<Object>) param;
                this.getMethod().invoke(queryWrapper, field, params.get(0), params.get(1));
            }
        },
        REGEXP(takeMethod("apply", String.class, Object[].class)) {
            /**
             * 正则表达式语句模板
             * */
            private static final String REGEX_TEMPLATE = "REGEXP {0}";

            /**
             * 正则表达式的实现是通过apply函数实现的
             * 要对字段做一个预处理
             * */
            @Override
            @SneakyThrows
            public <T> void execute(QueryWrapper<T> queryWrapper, String field, Object param) {
                // 这个函数执行完之后，SQL片段会变成 "field REGEXP ?"
                this.getMethod().invoke(queryWrapper, String.join(StringConstant.SPACE, field, REGEX_TEMPLATE), param);
            }

        },
        /**
         * apply函数，有SQL注入风险！慎用！
         */
        APPLY(takeMethod("apply", String.class, Object[].class)) {
            @Override
            @SneakyThrows
            public <T> void execute(QueryWrapper<T> queryWrapper, String field, Object param) {
                this.getMethod().invoke(queryWrapper, field, param);
            }
        },
        /**
         * json字段查询
         */
        JSON_CONTAINS(takeMethod("apply", String.class, Object[].class)) {
            private static final String JSON_CONTAINS_TEMPLATE = "JSON_CONTAINS(%s->'%s', '%s')";

            @Override
            @SneakyThrows
            public <T> void execute(QueryWrapper<T> queryWrapper, String field, String jsonKey, Object param) {
                this.getMethod().invoke(queryWrapper, String.format(JSON_CONTAINS_TEMPLATE, field, jsonKey, param), null);
            }
        };

        protected final Method method;

    }


    /**
     * 探机对象的扩展函数
     * 设计思路是让外界可以一定程度上干涉探机对象的处理逻辑
     * 干涉范围要求内部可以控制，所以用枚举来实现
     */
    public enum Extend {

        /**
         * 不做处理
         */
        NONE,
        /**
         * 忽略，跳过该探机的条件逻辑
         */
        SKIP,
        /**
         * 禁止后端修改这个数据
         */
        DO_NOT_OVERRIDE,
        /**
         * 日期类型处理
         * 以下处理逻辑是与前端的约定
         * 如果是EQ逻辑，把逻辑强行转为BETWEEN，把入参拆成两个，入参时间的00:00:00和入参时间的23:59:59
         * 如果是BETWEEN逻辑，第一个参数设置时间到日期的00:00:00，第二个则设定到23:59:59
         * 如果是GT/GE逻辑，把入参时间调到00:00:00
         * 如果是LT/LE逻辑，把入参时间调到23:59:59
         */
        DATE {
            /**
             * 日期类型要重写预处理函数
             * */
            @Override
            <T> Probe handle(Probe probe, Class<T> clazz) {

                // 与前端约定，所有的日期入参都以字符串的形式入参
                // 其格式为【yyyy-MM-dd】或【yyyy-MM-dd HH:mm:ss】
                switch (probe.getRule()) {
                    case EQ:
                        probe.setRule(Rule.BETWEEN);
                        probe.setValue(DateTimeKit.alignToBeginAndEnd((String) probe.getValue(), true));
                        break;
                    case BETWEEN:
                        var params = (List<String>) probe.getValue();
                        // 处理逻辑是这样的，如果一个时间是yyyy-MM-dd HH:mm:ss的格式，那就不做处理，以前端入参为准
                        // 否则，如果是第一个参数，日期设定到当天的00:00:00，第二个参数设定为23:59:59
                        probe.setValue(DateTimeKit.alignToBeginAndEnd(params.get(0), false, params.get(1), true));
                        break;
                    case GT:
                    case GE:
                        // 如果是GT/GE逻辑，把入参时间调到00:00:00
                        // 不强制覆盖，前端传递了时间的话，以前端为准
                        probe.setValue(DateTimeKit.alignToBegin((String) probe.getValue(), false));
                        break;
                    case LT:
                    case LE:
                        // 如果是LT/LE逻辑，把入参时间调到23:59:59
                        probe.setValue(DateTimeKit.alignToEnd((String) probe.getValue(), true));
                        break;
                    default:
                        break;
                }
                return probe;
            }
        },
        /**
         * 枚举值的标记，如果是枚举值，要把值转换一次
         * 约定，前端会传递字符串类型进来，后端转换成枚举值对应的数字value
         */
        ENUM {
            @Override
            <T> Probe handle(Probe probe, Class<T> clazz) {
                // 获取字段
                Class<?> enumType = ReflectionKit.getFieldMap(clazz).get(StringKit.camelCase(probe.getField())).getType();
                // 枚举的查询一般只有EQ、IN、NE、NOT_IN几种情况，所以就只处理这几种情况了，有其他的以后再加
                Rule rule = probe.getRule();
                if (rule == Rule.EQ || rule == Rule.NE) {
                    probe.setValue(EnumKit.translate2Code((String) probe.getValue(), enumType));
                }
                if (rule == Rule.IN || rule == Rule.NOT_IN) {
                    List<Integer> values = ((ArrayList<String>) probe.getValue()).stream().map(value -> EnumKit.translate2Code(value, enumType)).collect(Collectors.toList());
                    probe.setValue(values);
                }
                return probe;
            }
        };

        /**
         * 探机对象的处理函数
         * 默认情况下是不做处理的
         */
        <T> Probe handle(Probe probe, Class<T> clazz) {
            return probe;
        }

    }

    @Getter
    @AllArgsConstructor
    public enum Synapse implements MethodEnum {
        /**
         * 拼接AND条件
         */
        AND(takeMethod("and", Consumer.class)),
        /**
         * 拼接OR条件
         */
        OR(takeMethod("or"));

        private final Method method;
    }

    @Getter
    @AllArgsConstructor
    public enum Sorting implements MethodEnum {
        /**
         * 正序排序
         */
        ASC(takeMethod("orderByAsc", Object.class)),
        /**
         * 正序排序
         */
        ASCEND(takeMethod("orderByAsc", Object.class)),
        /**
         * 倒序排序
         */
        DESC(takeMethod("orderByDesc", Object.class)),
        /**
         * 倒序排序
         */
        DESCEND(takeMethod("orderByDesc", Object.class));

        private final Method method;

    }

    private interface MethodEnum {

        Method getMethod();

        /**
         * 函数的执行方法
         * 要想调用枚举对应的函数，必须使用这个函数来执行
         */
        @SneakyThrows
        default <T> void execute(QueryWrapper<T> queryWrapper) {
            this.getMethod().invoke(queryWrapper);
        }

        /**
         * 函数的执行方法
         * 要想调用枚举对应的函数，必须使用这个函数来执行
         */
        @SneakyThrows
        default <T> void execute(QueryWrapper<T> queryWrapper, String field) {
            execute(queryWrapper, field, null);
        }

        /**
         * 函数的执行方法
         * 要想调用枚举对应的函数，必须使用这个函数来执行
         */
        @SneakyThrows
        default <T> void execute(QueryWrapper<T> queryWrapper, String field, Object param) {
            execute(queryWrapper, field, null, param);
        }

        /**
         * 函数的执行方法
         * 要想调用枚举对应的函数，必须使用这个函数来执行
         *
         * @param queryWrapper 被执行的queryWrapper对象
         * @param field        字段
         * @param jsonKey      如果字段类型是json，这个是json的索引key（语法参考mysql官方文档），这个是个透传字段，通常情况这个字段是会被忽略的
         * @param param        参数对象
         */
        @SneakyThrows
        default <T> void execute(QueryWrapper<T> queryWrapper, String field, String jsonKey, Object param) {
            var method = this.getMethod();
            if (param != null) {
                method.invoke(queryWrapper, field, param);
            } else {
                method.invoke(queryWrapper, field);
            }
        }
    }

    /**
     * 从QueryWrapper中获取函数
     * 主要是做try-catch处理，因为要在构造器中初始化枚举类
     */
    @SneakyThrows
    private static Method takeMethod(String name, Class<?>... parameterTypes) {
        return QueryWrapper.class.getMethod(name, parameterTypes);
    }


    public static void main(String[] args) {
        Seeker<Entity> seeker = new Seeker<>();
        List<String> dateRange = Stream
                .of(LocalDateTime.now(), LocalDateTime.now())
                .map(DateTimeKit::format)
                .collect(Collectors.toList());
        seeker.addProbe(Seeker.Rule.BETWEEN, "create_date", dateRange, Seeker.Extend.DATE);
        QueryWrapper<Entity> entityQueryWrapper = seeker.buildWrapper(Entity.class);
        System.out.println(entityQueryWrapper.getSqlSegment());
        System.out.println(entityQueryWrapper.getTargetSql());
    }

}
